﻿using System;
using System.Reflection;
using System.Collections;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MugenInjection;
using MugenInjection.Exceptions;
using MugenInjection.Interception;
using MugenInjection.Interception.Interface;
using Should;

namespace MugenInjectionTest.Interception
{
    public class Alfa
    {

    }

    public class Bravo
    {

    }

    public class Omega
    {
        #region Reflection helpers

        public static readonly PropertyInfo VirtualPropertyInfo = typeof(Omega).GetProperty("VirtualProperty");

        public static readonly EventInfo VirtualEventInfo = typeof(Omega).GetEvent("VirtualEvent");

        public static readonly MethodInfo VirtualNonVoidMethodInfo = typeof(Omega).GetMethod("VirtualNonVoidMethod");

        public static readonly MethodInfo VirtualVoidMethodInfo = typeof(Omega).GetMethod("VirtualVoidMethod");

        public static readonly PropertyInfo NonVirtualPropertyInfo = typeof(Omega).GetProperty("NonVirtualProperty");

        public static readonly EventInfo NonVirtualEventInfo = typeof(Omega).GetEvent("NonVirtualEvent");

        public static readonly MethodInfo NonVirtualMethodInfo = typeof(Omega).GetMethod("NonVirtualMethod");

        #endregion


        public virtual string VirtualProperty { get; set; }

        public virtual event Action VirtualEvent;

        public Delegate[] Delegates
        {
            get
            {
                if (VirtualEvent == null) return null;
                return VirtualEvent.GetInvocationList();
            }
        }

        public virtual string VirtualNonVoidMethod()
        {
            return null;
        }

        public virtual void VirtualVoidMethod(string value)
        {
            return;
        }

        public string NonVirtualProperty { get; set; }

        public event Action<string> NonVirtualEvent;

        public void NonVirtualMethod() { }
    }

    public class InterceptAsTargetClass
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="T:System.Object"/> class.
        /// </summary>
        public InterceptAsTargetClass()
        {
            Id = Guid.NewGuid();
        }

        public Guid Id { get; set; }
    }

    public class SpyInterceptor : IInterceptorProcess
    {
        #region Property

        public IInterceptor LastCallInterceptor { get; set; }

        #endregion

        #region Implementation of IInterceptorProcess

        /// <summary>
        /// Intercept method.
        /// </summary>
        /// <param name="methodInterceptor">The specified <see cref="IMethodInterceptor"/>.</param>
        public void InterceptMethod(IMethodInterceptor methodInterceptor)
        {
            LastCallInterceptor = methodInterceptor;
        }

        /// <summary>
        /// Intercept get property method.
        /// </summary>
        /// <param name="propertyGetInterceptor">The specified <see cref="IPropertyGetInterceptor"/></param>
        public void InterceptGetProperty(IPropertyGetInterceptor propertyGetInterceptor)
        {
            LastCallInterceptor = propertyGetInterceptor;
        }

        /// <summary>
        /// Intercept set property method.
        /// </summary>
        /// <param name="propertySetInterceptor">The specified <see cref="IPropertySetInterceptor"/>.</param>
        public void InterceptSetProperty(IPropertySetInterceptor propertySetInterceptor)
        {
            LastCallInterceptor = propertySetInterceptor;
        }

        /// <summary>
        /// Intercept add event method.
        /// </summary>
        /// <param name="eventAddInterceptor">The specified <see cref="IEventAddInterceptor"/>.</param>
        public void InterceptAddEvent(IEventAddInterceptor eventAddInterceptor)
        {
            LastCallInterceptor = eventAddInterceptor;
        }

        /// <summary>
        /// Intercept remove event method.
        /// </summary>
        /// <param name="eventRemoveInterceptor">The specified <see cref="IEventRemoveInterceptor"/>.</param>
        public void InterceptRemoveEvent(IEventRemoveInterceptor eventRemoveInterceptor)
        {
            LastCallInterceptor = eventRemoveInterceptor;
        }

        /// <summary>
        /// Interceptor priority.
        /// </summary>
        public int Priority { get { return 0; } }

        #endregion
    }

    [TestClass]
    public class InterceptionExtensionUtilsTest : InterceptionTestBase
    {
        [TestMethod]
        public void InterceptTest()
        {
            using (var injector = GetInjector())
            {
                var spyInterceptor = new SpyInterceptor();
                InterceptionExtensionUtils.Intercept(injector.Bind<IDisposable>(), spyInterceptor);
                var disposable = injector.Get<IDisposable>();
                disposable.GetType().ShouldNotEqual(typeof(IDisposable));
                spyInterceptor.LastCallInterceptor.ShouldBeNull();
                disposable.Dispose();

                spyInterceptor.LastCallInterceptor.ShouldNotBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(IDisposable).GetMethod("Dispose"));
            }

            using (var injector = GetInjector())
            {
                var spyInterceptor = new SpyInterceptor();
                InterceptionExtensionUtils.Intercept(injector.Bind(typeof(IDisposable)), spyInterceptor);
                var disposable = injector.Get<IDisposable>();
                disposable.GetType().ShouldNotEqual(typeof(IDisposable));
                spyInterceptor.LastCallInterceptor.ShouldBeNull();
                disposable.Dispose();

                spyInterceptor.LastCallInterceptor.ShouldNotBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(IDisposable).GetMethod("Dispose"));
            }

            using (var injector = GetInjector())
            {
                var spyInterceptor = new SpyInterceptor();
                InterceptionExtensionUtils.Intercept(injector.Bind<IDisposable>(), new Type[] { typeof(ICloneable) }, spyInterceptor);
                var disposable = injector.Get<IDisposable>();
                disposable.GetType().ShouldNotEqual(typeof(IDisposable));
                spyInterceptor.LastCallInterceptor.ShouldBeNull();
                disposable.Dispose();

                spyInterceptor.LastCallInterceptor.ShouldNotBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(IDisposable).GetMethod("Dispose"));

                var cloneable = disposable as ICloneable;
                cloneable.ShouldNotBeNull();
                var clone = cloneable.Clone();
                clone.ShouldBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(ICloneable).GetMethod("Clone"));
            }

            using (var injector = GetInjector())
            {
                var spyInterceptor = new SpyInterceptor();
                InterceptionExtensionUtils.Intercept(injector.Bind(typeof(IDisposable)), new Type[] { typeof(ICloneable) }, spyInterceptor);
                var disposable = injector.Get<IDisposable>();
                disposable.GetType().ShouldNotEqual(typeof(IDisposable));
                spyInterceptor.LastCallInterceptor.ShouldBeNull();
                disposable.Dispose();

                spyInterceptor.LastCallInterceptor.ShouldNotBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(IDisposable).GetMethod("Dispose"));

                var cloneable = disposable as ICloneable;
                cloneable.ShouldNotBeNull();
                var clone = cloneable.Clone();
                clone.ShouldBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(ICloneable).GetMethod("Clone"));
            }
        }

        [TestMethod]
        public void InterceptAsTargetTest()
        {
            //Generic syntax
            using (var injector = GetInjector())
            {
                var spyInterceptor = new SpyInterceptor();
                InterceptionExtensionUtils.InterceptAsTarget(injector.Bind<IDisposable>().ToConstant(injector), spyInterceptor);
                var disposable = injector.Get<IDisposable>();
                disposable.Dispose();

                spyInterceptor.LastCallInterceptor.ShouldNotBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(IDisposable).GetMethod("Dispose"));
                spyInterceptor.LastCallInterceptor.Target.ShouldEqual(injector);
            }

            //Non-generic syntax
            using (var injector = GetInjector())
            {
                var spyInterceptor = new SpyInterceptor();
                InterceptionExtensionUtils.InterceptAsTarget(injector.Bind(typeof(IDisposable)).ToConstant(injector), spyInterceptor);
                var disposable = injector.Get<IDisposable>();
                disposable.Dispose();

                spyInterceptor.LastCallInterceptor.ShouldNotBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(IDisposable).GetMethod("Dispose"));
                spyInterceptor.LastCallInterceptor.Target.ShouldEqual(injector);
            }

            //Manual build generic syntax
            using (var injector = GetInjector())
            {
                var spyInterceptor = new SpyInterceptor();
                InterceptionExtensionUtils.InterceptAsTarget(injector.BindWithManualBuild<IDisposable>().ToConstant(injector), spyInterceptor).TryDisposeObjects().Build();
                var disposable = injector.Get<IDisposable>();
                disposable.Dispose();

                spyInterceptor.LastCallInterceptor.ShouldNotBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(IDisposable).GetMethod("Dispose"));
                spyInterceptor.LastCallInterceptor.Target.ShouldEqual(injector);
            }

            //Manual build non-generic syntax
            using (var injector = GetInjector())
            {
                var spyInterceptor = new SpyInterceptor();
                InterceptionExtensionUtils.InterceptAsTarget(injector.BindWithManualBuild(typeof(IDisposable)).ToConstant(injector), spyInterceptor).TryDisposeObjects().Build();
                var disposable = injector.Get<IDisposable>();
                disposable.Dispose();

                spyInterceptor.LastCallInterceptor.ShouldNotBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(IDisposable).GetMethod("Dispose"));
                spyInterceptor.LastCallInterceptor.Target.ShouldEqual(injector);
            }

            using (var injector = GetInjector())
            {
                var spyInterceptor = new SpyInterceptor();
                InterceptionExtensionUtils.InterceptAsTarget(injector.Bind<IDisposable>().ToConstant(injector), new Type[] { typeof(ICloneable) }, spyInterceptor);
                var disposable = injector.Get<IDisposable>();
                disposable.Dispose();
                spyInterceptor.LastCallInterceptor.ShouldNotBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(IDisposable).GetMethod("Dispose"));
                spyInterceptor.LastCallInterceptor.Target.ShouldEqual(injector);

                var cloneable = disposable as ICloneable;
                cloneable.ShouldNotBeNull();
                var clone = cloneable.Clone();
                clone.ShouldBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(ICloneable).GetMethod("Clone"));
                spyInterceptor.LastCallInterceptor.Target.ShouldEqual(injector);
            }

            using (var injector = GetInjector())
            {
                var spyInterceptor = new SpyInterceptor();
                InterceptionExtensionUtils.InterceptAsTarget(injector.Bind(typeof(IDisposable)).ToConstant(injector), new Type[] { typeof(ICloneable) }, spyInterceptor);
                var disposable = injector.Get<IDisposable>();
                disposable.Dispose();
                spyInterceptor.LastCallInterceptor.ShouldNotBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(IDisposable).GetMethod("Dispose"));
                spyInterceptor.LastCallInterceptor.Target.ShouldEqual(injector);

                var cloneable = disposable as ICloneable;
                cloneable.ShouldNotBeNull();
                var clone = cloneable.Clone();
                clone.ShouldBeNull();
                spyInterceptor.LastCallInterceptor.IsAbstract.ShouldBeTrue();
                spyInterceptor.LastCallInterceptor.Member.ShouldEqual(typeof(ICloneable).GetMethod("Clone"));
                spyInterceptor.LastCallInterceptor.Target.ShouldEqual(injector);
            }

        }

        [TestMethod]
        public void ConditionInterceptTest()
        {
            var injector = GetInjector();
            var spyInterceptor = new SpyInterceptor();
            TestUtils.Catch<NotSupportedException>(
                () => InterceptionExtensionUtils.Intercept(injector.Bind<Alfa, Bravo>(), spyInterceptor));

        }

        [TestMethod]
        public void ActionInterceptNonVirtualMethodsTest()
        {
            using (var injector = GetInjector())
            {
                TestUtils.Catch<ArgumentException>(() => InterceptionExtensionUtils
                                                             .InterceptGetProperty(InterceptionExtensionUtils
                                                                                       .Intercept(injector.Bind<Omega>()), Omega.NonVirtualPropertyInfo, interceptor => null));
                TestUtils.Catch<ArgumentException>(() => InterceptionExtensionUtils
                                                            .InterceptSetProperty(InterceptionExtensionUtils
                                                                                      .Intercept(injector.Bind<Omega>()), Omega.NonVirtualPropertyInfo, interceptor => null));
                TestUtils.Catch<ArgumentException>(() => InterceptionExtensionUtils
                                                            .InterceptAddEvent(InterceptionExtensionUtils
                                                                                      .Intercept(injector.Bind<Omega>()), Omega.NonVirtualEventInfo, interceptor => null));
                TestUtils.Catch<ArgumentException>(() => InterceptionExtensionUtils
                                                            .InterceptRemoveEvent(InterceptionExtensionUtils
                                                                                      .Intercept(injector.Bind<Omega>()), Omega.NonVirtualEventInfo, interceptor => null));
                TestUtils.Catch<ArgumentException>(() => InterceptionExtensionUtils
                                                            .InterceptInvokeMethod(InterceptionExtensionUtils
                                                                                      .Intercept(injector.Bind<Omega>()), Omega.NonVirtualMethodInfo, interceptor => null));
#if !NOEXPRESSION
                TestUtils.Catch<ArgumentException>(() => injector.Bind<Omega>().Intercept().InterceptGetProperty(omega => omega.NonVirtualProperty, (target, value, interceptor) => null));
                TestUtils.Catch<ArgumentException>(() => injector.Bind<Omega>().Intercept().InterceptSetProperty(omega => omega.NonVirtualProperty, (target, value, interceptor) => null));
                TestUtils.Catch<ArgumentException>(() => injector.Bind<Omega>().Intercept().InterceptInvokeMethod((context, omega) => omega.NonVirtualMethod(), (target, original, interceptor) => { }));
#endif

            }
        }

        [TestMethod]
        public void ActionInterceptVirtualPropertiesTest()
        {
            using (var injector = GetInjector())
            {
                const string setProp = "set";
                const string propertyToGet = "propertyToGet";
                const string propertyToSet = "propertyToSet";
                var syntax = InterceptionExtensionUtils.Intercept(injector.Bind<Omega>());
                InterceptionExtensionUtils.InterceptGetProperty(syntax, Omega.VirtualPropertyInfo, interceptor =>
                    {
                        interceptor.GetMethod();
                        propertyToSet.ShouldEqual(interceptor.GetValue);
                        return propertyToGet;
                    });
                InterceptionExtensionUtils.InterceptSetProperty(syntax, Omega.VirtualPropertyInfo, interceptor =>
                    {
                        setProp.ShouldEqual(interceptor.SetValue);
                        return propertyToSet;
                    });

                //Checking duplicate members.
                TestUtils.Catch<InvalidBindingException>(() => InterceptionExtensionUtils.InterceptGetProperty(syntax, Omega.VirtualPropertyInfo, interceptor =>
                    {
                        interceptor.GetMethod();
                        propertyToSet.ShouldEqual(interceptor.GetValue);
                        return propertyToGet;
                    }));
                TestUtils.Catch<InvalidBindingException>(() => InterceptionExtensionUtils.InterceptSetProperty(syntax, Omega.VirtualPropertyInfo, interceptor =>
                    {
                        setProp.ShouldEqual(interceptor.SetValue);
                        return propertyToSet;
                    }));

                var omega = injector.Get<Omega>();
                omega.VirtualProperty = setProp;
                omega.VirtualProperty.ShouldEqual(propertyToGet);
            }
#if !NOEXPRESSION
            using (var injector = GetInjector())
            {
                const string setProp = "set";
                const string propertyToGet = "propertyToGet";
                const string propertyToSet = "propertyToSet";
                injector.Bind<Omega>()
                    .Intercept()
                    .InterceptGetProperty(omega1 => omega1.VirtualProperty, (target, value, interceptor) =>
                        {
                            target.ShouldBeNull();
                            propertyToSet.ShouldEqual(value());
                            return propertyToGet;
                        })
                    .InterceptSetProperty(omega1 => omega1.VirtualProperty, (target, value, interceptor) =>
                        {
                            target.ShouldBeNull();
                            setProp.ShouldEqual(value);
                            return propertyToSet;
                        });

                var omega = injector.Get<Omega>();
                omega.VirtualProperty = setProp;
                omega.VirtualProperty.ShouldEqual(propertyToGet);
            }
#endif
        }

        [TestMethod]
        public void ActionInterceptVirtualMethodsTest()
        {
            using (var injector = GetInjector())
            {
                const string inputParameter = "inputParameter";
                const string outputParameter = "outputParameter";
                var syntax = InterceptionExtensionUtils.Intercept(injector.Bind<Omega>());
                InterceptionExtensionUtils.InterceptInvokeMethod(syntax, Omega.VirtualVoidMethodInfo, interceptor =>
                    {
                        interceptor.InputParameters[0].ShouldEqual(inputParameter);
                        return null;
                    });
                InterceptionExtensionUtils.InterceptInvokeMethod(syntax, Omega.VirtualNonVoidMethodInfo, interceptor =>
                    {
                        return outputParameter;
                    });
                TestUtils.Catch<InvalidBindingException>(() => InterceptionExtensionUtils.InterceptInvokeMethod(syntax, Omega.VirtualNonVoidMethodInfo, interceptor => outputParameter));

                var omega = injector.Get<Omega>();
                omega.VirtualNonVoidMethod().ShouldEqual(outputParameter);
                omega.VirtualVoidMethod(inputParameter);
            }
#if !NOEXPRESSION
            using (var injector = GetInjector())
            {
                const string inputParameter = "inputParameter";
                const string outputParameter = "outputParameter";
                injector.Bind<Omega>()
                    .Intercept()
                    .InterceptInvokeMethod((context, omega1) => omega1.VirtualVoidMethod(context.Any<string>()),
                                           (target, original, interceptor) =>
                                           {
                                               target.ShouldBeNull();
                                               interceptor.InputParameters[0].ShouldEqual(inputParameter);
                                           })
                    .InterceptInvokeMethod((context, omega1) => omega1.VirtualNonVoidMethod(),
                                           (target, original, interceptor) =>
                                           {
                                               target.ShouldBeNull();
                                               original().ShouldBeNull();
                                               return outputParameter;
                                           });
                var omega = injector.Get<Omega>();
                omega.VirtualNonVoidMethod().ShouldEqual(outputParameter);
                omega.VirtualVoidMethod(inputParameter);
            }
#endif

        }

        [TestMethod]
        public void EventInterceptVirtualEventsTest()
        {
            var actionAdd = new Action(() => { });
            var actionRemove = new Action(() => { });
            using (var injector = GetInjector())
            {
                var syntax = InterceptionExtensionUtils.Intercept(injector.Bind<Omega>());
                InterceptionExtensionUtils.InterceptAddEvent(syntax, Omega.VirtualEventInfo, interceptor =>
                    {
                        actionAdd.ShouldEqual(interceptor.AddValue);
                        return actionAdd;
                    });
                InterceptionExtensionUtils.InterceptRemoveEvent(syntax, Omega.VirtualEventInfo, interceptor =>
                    {
                        actionRemove.ShouldEqual(interceptor.RemoveValue);
                        return actionRemove;
                    });

                //Checking duplicate members.
                TestUtils.Catch<InvalidBindingException>(
                    () =>
                    InterceptionExtensionUtils.InterceptAddEvent(syntax, Omega.VirtualEventInfo, interceptor =>
                        {
                            actionAdd.ShouldEqual(interceptor.AddValue);
                            return actionAdd;
                        }));
                TestUtils.Catch<InvalidBindingException>(
                    () =>
                    InterceptionExtensionUtils.InterceptRemoveEvent(syntax, Omega.VirtualEventInfo, interceptor =>
                        {
                            actionRemove.ShouldEqual(interceptor.RemoveValue);
                            return actionRemove;
                        }));

                var omega = injector.Get<Omega>();
                omega.VirtualEvent += actionAdd;
                omega.VirtualEvent -= actionRemove;
                omega.Delegates.ShouldNotBeEmpty();
                omega.Delegates[0].ShouldEqual(actionAdd);
            }
        }
    }

#if SILVERLIGHT
    public interface ICloneable
    {
        object Clone();
    }
#endif

#if NETFX_CORE
    public interface ICloneable
    {
        object Clone();
    }
#endif

}